using System;
using System.Collections;
using System.Reflection;
using NHibernate.Criterion;
using Sedna.Core.Persistence;
using log4net;
using NHibernate;
using NHibernate.Engine;

namespace Sedna.Core.Persistence
{
	/// <summary>
	/// this is an implementation of a IFilterCriteriaBuilder that 
	/// takes an instance of a <see cref="IExternalCriteriaFilterState"/> and 
	/// inspects its propertis. It looks for those properties that have
	/// <see cref="FilterPropertyAttribute"/> defined on them and creates a 
	/// ICriteria that corresponds the internal state of a filter.
	/// </summary>
	public class AttributeBasedFilterCriteriaBuilder : IFilterCriteriaBuilder
	{

		private ILog log = LogManager.GetLogger(typeof(AttributeBasedFilterCriteriaBuilder));

		/// <summary>
		/// implementation of a corresponding interface method
		/// this method takes only instances of the <see cref="IExternalCriteriaFilterState"/>
		/// </summary>
		/// <param name="filterState">an instance of <see cref="IExternalCriteriaFilterState"/></param>
		/// <param name="type">type of a persistent entity</param>
		/// <param name="session">an instance of a valid and opened NHibernate ISession</param>
		/// <returns>an instance of a NHibernate ICriteria that corresponds the passed filter state</returns>
		public ICriteria Build(IFilterState filterState, Type type, ISession session)
		{
			ArrayList propertyPairs = new ArrayList();

			ICriteria criteria = session.CreateCriteria(type);
			PropertyInfo[] properties = filterState.GetType().GetProperties();
			foreach(PropertyInfo propertyInfo in properties)
			{
				if(propertyInfo.GetCustomAttributes(typeof(FilterPropertyAttribute), true).Length == 0)
				{
					continue;
				}

				if (!IsRangedProperty(propertyInfo))
				{
					ProcessProperty(propertyInfo, criteria, filterState);
				}
				else
				{
					ProcessRangedProperty(propertyInfo, criteria, filterState, propertyPairs);
				}

			}

			criteria.SetFirstResult(filterState.StartIndex);
			
			if (filterState.Amount >= 0)
			{
				criteria.SetMaxResults(filterState.Amount);
			}
			else
			{
				criteria.SetMaxResults(RowSelection.NoValue);
			}

			ApplyOrdering(criteria, filterState);

			return criteria;
		}

		private void ProcessProperty(PropertyInfo propertyInfo, ICriteria criteria, IFilterState filterState)
		{
			FilterPropertyAttribute attribute = (FilterPropertyAttribute)propertyInfo.GetCustomAttributes(typeof(FilterPropertyAttribute), true)[0];

			object value = propertyInfo.GetValue(filterState, null);
			if(CheckIfUnset(attribute, value) && attribute.FilterUnsetStrategy == FilterUnsetStrategy.Skip)
			{
				return;
			}

			string fieldName = attribute.FieldName!=null?attribute.FieldName:propertyInfo.Name;


			if(CheckIfUnset(attribute, value) && attribute.FilterUnsetStrategy == FilterUnsetStrategy.AddIsNull)
			{
				AddIsNullPropertyValue(criteria, fieldName);
			}
			else
			{
				AddPropertyValue(attribute, propertyInfo, fieldName, criteria, value);
			}
		}

		private void ProcessRangedProperty(PropertyInfo propertyInfo, ICriteria criteria, IFilterState filterState, ArrayList propertyPairs)
		{
			FilterPropertyAttribute attribute = (FilterPropertyAttribute)propertyInfo.GetCustomAttributes(typeof(FilterPropertyAttribute), true)[0];

			PropertyInfo propertyPair = GetRangedPropertyPair(propertyPairs, attribute);
			if (propertyPair != null)
			{
				object loValue = GetRangeFrom(attribute, propertyInfo, propertyPair).GetValue(filterState, null);
				object hiValue = GetRangeTo(attribute, propertyInfo, propertyPair).GetValue(filterState, null);
				if(CheckIfUnset(attribute, loValue) || CheckIfUnset(attribute, hiValue))
				{
					return;
				}

				string fieldName = attribute.FieldName;
				criteria.Add(Expression.Between(fieldName, loValue, hiValue));

				propertyPairs.Remove(propertyPair);
			}
			else
			{
				propertyPairs.Add(propertyInfo);
			}
		}

		private bool CheckIfUnset(FilterPropertyAttribute attribute, object value)
		{
			return attribute.UnsetValue == null && value == null || attribute.UnsetValue != null && attribute.UnsetValue.Equals(value);
		}

		private void AddPropertyValue(FilterPropertyAttribute attribute, PropertyInfo propertyInfo, string fieldName, ICriteria criteria, object value)
		{
			FilterEquality equality = attribute.FilterEquality;
			if(equality == FilterEquality.Default)
			{
				equality = (typeof(string).IsAssignableFrom(propertyInfo.PropertyType))?FilterEquality.FullLike:FilterEquality.Eq;
			}

			switch(equality)
			{
				case FilterEquality.FullLike:
					criteria.Add(Expression.Like(fieldName, "%"+value+"%"));
					break;

				case FilterEquality.Eq:
					criteria.Add(Expression.Eq(fieldName, value));
					break;

				case FilterEquality.In:
					criteria.Add(Expression.In(fieldName, (ICollection)value));
					break;

				default:
					throw new PstStateException("Unsupported FilterEquality:" + equality);
			}
		}

		private void AddIsNullPropertyValue(ICriteria criteria, string fieldName)
		{
			criteria.Add(Expression.IsNull(fieldName));
		}


		private void ApplyOrdering(ICriteria criteria, IFilterState filterState)
		{
			if(filterState.SortExpression == null)
			{
				return;
			}
			criteria.AddOrder(filterState.IsAsc?Order.Asc(filterState.SortExpression):Order.Desc(filterState.SortExpression));
		}

		private bool IsRangedProperty(PropertyInfo propertyInfo)
		{
			FilterPropertyAttribute attribute = GetFilterPropertyAttribute(propertyInfo);
			return attribute.FilterEquality == FilterEquality.RangeFrom ||
				attribute.FilterEquality == FilterEquality.RangeTo;
		}

		private FilterPropertyAttribute GetFilterPropertyAttribute(PropertyInfo propertyInfo)
		{
			return (FilterPropertyAttribute)(propertyInfo.GetCustomAttributes(typeof(FilterPropertyAttribute), true)[0]);
		}

		private PropertyInfo GetRangedPropertyPair(ArrayList propertyPairs, FilterPropertyAttribute attribute)
		{
			FilterEquality filterEqualityPair = GetRangeEqualityPair(attribute.FilterEquality);
			foreach (PropertyInfo propertyInfo in propertyPairs)
			{
				//PropertyInfo propertyInfo = (PropertyInfo)propertyPair;
				FilterPropertyAttribute pairAttribute = GetFilterPropertyAttribute(propertyInfo);
				if (pairAttribute.FilterEquality == filterEqualityPair &&
					pairAttribute.FieldName.Equals(attribute.FieldName))
				{
					return propertyInfo;
				}
			}
			return null;
		}

		private FilterEquality GetRangeEqualityPair(FilterEquality filterEquality)
		{
			return filterEquality == FilterEquality.RangeFrom ? 
				FilterEquality.RangeTo : FilterEquality.RangeFrom;
		}

		private PropertyInfo GetRangeFrom(FilterPropertyAttribute attribute, PropertyInfo propertyInfo, PropertyInfo propertyPair)
		{
			return (attribute.FilterEquality == FilterEquality.RangeFrom) ? propertyInfo : propertyPair;
		}

		private PropertyInfo GetRangeTo(FilterPropertyAttribute attribute, PropertyInfo propertyInfo, PropertyInfo propertyPair)
		{
			return (attribute.FilterEquality == FilterEquality.RangeTo) ? propertyInfo : propertyPair;
		}


	}
}
